// -----------------------------------------------------------------------
//  <copyright file="AuthorizationController.cs" company="LiuliuSoft">
//      Copyright (c) 2022-2025 DaprPlus. All rights reserved.
//  </copyright>
//  <site>https://dapr.plus</site>
//  <last-editor>郭明锋</last-editor>
//  <last-date>2025/1/15 0:54</last-date>
// -----------------------------------------------------------------------

using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Server;
using OpenIddict.Abstractions;
using Microsoft.Extensions.Options;

using DaprPlus.AspNetCore.Helpers;
using DaprPlus.Identity.Domain.Entities;
using DaprPlus.Identity.Api.Models;
using DaprPlus.Identity.Application;
using DaprPlus.Identity.Application.Identity.Users.Commands;

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Primitives;

using OpenIddict.Server.AspNetCore;

using OSharp.Security;

using static OpenIddict.Abstractions.OpenIddictConstants;


namespace DaprPlus.Identity.Api.Controllers;

[Route("connect")]
public class AuthorizationController(
    IOpenIddictApplicationManager applicationManager,
    IOpenIddictAuthorizationManager authorizationManager,
    IOpenIddictScopeManager scopeManager,
    IOpenIddictTokenManager tokenManager,
    UserManager<User> userManager,
    SignInManager<User> signInManager,
    ILogger<AuthorizationController> logger) : Controller
{
    protected IServiceProvider ServiceProvider => HttpContext.RequestServices;

    [HttpGet("authorize"), HttpPost("authorize")]
    public async Task<ActionResult> Authorize()
    {
        var request = HttpContext.GetOpenIddictServerRequest() ??
            throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

        var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        if (!result.Succeeded || request.HasPromptValue(PromptValues.Login)
            || (request.MaxAge != null && result.Properties?.IssuedUtc != null
                && DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value)))
        {
            /* 这段代码检查以下几个条件：
            result.Succeeded：用户是否已经成功认证。
            request.HasPromptValue(PromptValues.Login)：请求中是否包含 login 提示，要求用户重新登录。
            检查用户的登录时间是否超过了 MaxAge 的限制。如果 MaxAge 不为 null，且用户的 IssuedUtc 时间存在，且当前时间与该时间的差值超过了 MaxAge 的时间，表示用户的会话已过期。
            */
            if (request.HasPromptValue(PromptValues.None))
            {
                // 如果请求中包含 none 提示，表示不希望用户看到登录界面，而是直接返回错误信息。此时会返回一个 Forbid 响应，说明用户需要登录。
                return Forbid(Errors.LoginRequired, "The user is not logged in.");
            }

            // 从请求中获取所有的提示参数，移除 login 提示，准备重定向用户到登录页面。
            var prompt = string.Join(" ", request.GetPromptValues().Remove(PromptValues.Login));
            var parameters = Request.HasFormContentType
                ? Request.Form.Where(p => p.Key != Parameters.Prompt).ToList()
                : Request.Query.Where(p => p.Key != Parameters.Prompt).ToList();
            parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));

            // 使用 Challenge 方法重定向用户去登录。RedirectUri 设置为当前请求的基础路径和路径，并附加新的查询字符串参数，以便在用户成功登录后能够返回到原请求的上下文。
            var redirectUrl = Request.PathBase + Request.Path + QueryString.Create(parameters);
            return Challenge(new AuthenticationProperties()
            {
                RedirectUri = redirectUrl
            }, CookieAuthenticationDefaults.AuthenticationScheme);
        }

        var user = await GetUserAsync(result.Principal);
        var application = await GetApplicationAsync(request.ClientId!);

        // 检索与用户和调用客户端应用程序关联的永久授权。
        var authorizations = await GetAuthorizations(user, application, request);

        // 根据同意类型处理授权。
        var consentType = await applicationManager.GetConsentTypeAsync(application);
        switch (consentType)
        {
            // 如果同意是外部的（例如，当系统管理员授予权限时），如果在数据库中找不到权限，则立即返回错误。
            case ConsentTypes.External when authorizations.Count == 0:
                return Forbid(Errors.ConsentRequired, "The logged in user is not allowed to access this client application.");

            case ConsentTypes.Implicit:
            case ConsentTypes.External when authorizations.Count > 0:
            case ConsentTypes.Explicit when authorizations.Count > 0 && !request.HasPromptValue(PromptValues.Consent):
                var identity = await CreateClaimsIdentity(request, user, application, authorizations);
                return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

            case ConsentTypes.Explicit when request.HasPromptValue(PromptValues.None):
            case ConsentTypes.Systematic when request.HasPromptValue(PromptValues.None):
                return Forbid(Errors.ConsentRequired, "Interactive user consent is required.");

            // 在其他所有情况下，显示同意表格。
            default:
                var model = new AuthorizeViewModel()
                {
                    ApplicationName = await applicationManager.GetLocalizedDisplayNameAsync(application),
                    Scope = request.Scope
                };
                return View(model);
        }
    }

    [Authorize, FormValueRequired("submit.Accept")]
    [HttpPost("authorize"), ValidateAntiForgeryToken]
    public async Task<ActionResult> Accept()
    {
        var request = HttpContext.GetOpenIddictServerRequest() ??
            throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

        var user = await GetUserAsync(User);
        var application = await GetApplicationAsync(request.ClientId!);
        var authorizations = await GetAuthorizations(user, application, request);

        if (authorizations.Count is 0 && await applicationManager.HasConsentTypeAsync(application, ConsentTypes.External))
        {
            return Forbid(Errors.ConsentRequired, "The logged in user is not allowed to access this client application.");
        }

        var identity = await CreateClaimsIdentity(request, user, application, authorizations);
        return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    }

    [Authorize, FormValueRequired("submit.Deny")]
    [HttpPost("authorize"), ValidateAntiForgeryToken]
    public ActionResult Deny() => Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

    // 交换代码和令牌
    [HttpPost("token"), IgnoreAntiforgeryToken, Produces("application/json")]
    public async Task<ActionResult> Exchange()
    {
        var request = HttpContext.GetOpenIddictServerRequest() ??
            throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

        if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
        {
            var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            var userId = result.Principal!.FindFirstValue(Claims.Subject);
            var user = await userManager.FindByIdAsync(userId!);
            if (user == null)
            {
                return Forbid(Errors.InvalidGrant, "The token is no longer valid.");
            }

            if (!await signInManager.CanSignInAsync(user))
            {
                return Forbid(Errors.InvalidGrant, "The user is no longer allowed to sign in.");
            }

            var identity = await CreateClaimsIdentity(result.Principal!, user);
            return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        throw new NotSupportedException("The specified grant type is not supported.");
    }

    [HttpGet("endsession"), HttpPost("endsession"), IgnoreAntiforgeryToken]
    public async Task<ActionResult> EndSession()
    {
        // 获取 OIDC 请求参数
        var request = HttpContext.GetOpenIddictServerRequest() ??
            throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

        // 执行登出命令
        var command = new LogoutCommand() { Principal = User };
        var mediator = ServiceProvider.GetRequiredService<IMediator>();
        await mediator.Send(command);

        // 获取 post_logout_redirect_uri
        var postLogoutRedirectUri = request.PostLogoutRedirectUri;
        if (string.IsNullOrEmpty(postLogoutRedirectUri))
        {
            postLogoutRedirectUri = "/";
        }

        // 验证 id_token_hint（如果提供）
        if (!string.IsNullOrEmpty(request.IdTokenHint))
        {
            try
            {
                // 获取当前用户的 sub claim
                var currentUserSub = User.GetClaim(ClaimTypes.NameIdentifier);
                if (currentUserSub == null)
                {
                    // 执行登出操作
                    return SignOut(
                        authenticationSchemes:
                        [
                            CookieAuthenticationDefaults.AuthenticationScheme,
                            OpenIddictServerAspNetCoreDefaults.AuthenticationScheme
                        ],
                        properties: new AuthenticationProperties
                        {
                            RedirectUri = postLogoutRedirectUri
                        });
                }

                // 获取签名密钥
                var options = ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictServerOptions>>().CurrentValue;
                var signingCredentials = options.SigningCredentials.First();
                var issuer = options.Issuer?.AbsoluteUri ?? $"{Request.Scheme}://{Request.Host.Value}/";

                // 验证 id_token_hint
                var tokenHandler = new JwtSecurityTokenHandler();
                var parameters = new TokenValidationParameters
                {
                    ValidIssuer = issuer,
                    ValidateAudience = false, // 不验证 audience，因为我们会从 token 中获取它
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = signingCredentials.Key,
                    ValidateLifetime = false // id_token_hint 不需要验证过期时间
                };

                ClaimsPrincipal result;
                try
                {
                    result = tokenHandler.ValidateToken(request.IdTokenHint, parameters, out var validatedToken);

                    // 从 token 中获取 audience（即 client_id）
                    var jwtToken = validatedToken as JwtSecurityToken;
                    var clientId = jwtToken?.Audiences.FirstOrDefault();

                    if (string.IsNullOrEmpty(clientId))
                    {
                        return BadRequest(Errors.InvalidToken, "The id_token_hint does not contain a valid audience.");
                    }

                    // 验证 client_id 是否存在
                    var application = await applicationManager.FindByClientIdAsync(clientId);
                    if (application == null)
                    {
                        return BadRequest(Errors.InvalidToken, "The application related to id_token_hint could not be found.");
                    }

                    var applicationId = await applicationManager.GetIdAsync(application);
                    var count = await tokenManager.RevokeAsync(currentUserSub, applicationId, Statuses.Valid, null);
                    logger.LogInformation("Revoked {Count} tokens for user {UserId} and application {ApplicationId}.", count, currentUserSub, applicationId);
                }
                catch (Exception)
                {
                    return BadRequest(Errors.InvalidToken, "The id_token_hint parameter is invalid.");
                }

                // 验证 sub claim 是否匹配当前用户
                var tokenSub = result.FindFirstValue(ClaimTypes.NameIdentifier);
                if (string.IsNullOrEmpty(tokenSub) || tokenSub != currentUserSub)
                {
                    //return BadRequest(Errors.InvalidToken, "The id_token_hint subject does not match the current user.");
                }
            }
            catch (Exception ex)
            {
                return BadRequest(Errors.InvalidToken, $"Failed to validate id_token_hint: {ex.Message}");
            }
        }

        // 执行登出操作
        return SignOut(
            authenticationSchemes:
            [
                CookieAuthenticationDefaults.AuthenticationScheme,
                OpenIddictServerAspNetCoreDefaults.AuthenticationScheme
            ],
            properties: new AuthenticationProperties
            {
                RedirectUri = postLogoutRedirectUri
            });
    }

    [HttpGet("userinfo"), HttpPost("userinfo"), Produces("application/json")]
    [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
    [ApiResultWrapperFilter]
    public async Task<ActionResult> Userinfo()
    {
        var user = await userManager.FindByIdAsync(User.GetClaim(Claims.Subject)!);
        if (user == null)
        {
#pragma warning disable CS8620 // 由于引用类型的可为 null 性差异，实参不能用于形参。
            return Challenge(
                authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                properties: new AuthenticationProperties(new Dictionary<string, string>
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken,
                    [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user profile is no longer available."
                }));
#pragma warning restore CS8620 // 由于引用类型的可为 null 性差异，实参不能用于形参。
        }

        var claims = new Dictionary<string, object?>(StringComparer.Ordinal)
        {
            [Claims.Subject] = await userManager.GetUserIdAsync(user),
            [Claims.Name] = await userManager.GetUserNameAsync(user),
            [Claims.Nickname] = user.NickName,
            [Claims.Picture] = user.Avatar,
        };

        if (User.HasScope(Scopes.Email))
        {
            claims[Claims.Email] = await userManager.GetEmailAsync(user);
            claims[Claims.EmailVerified] = await userManager.IsEmailConfirmedAsync(user);
        }

        if (User.HasScope(Scopes.Phone))
        {
            claims[Claims.PhoneNumber] = await userManager.GetPhoneNumberAsync(user);
            claims[Claims.PhoneNumberVerified] = await userManager.IsPhoneNumberConfirmedAsync(user);
        }

        if (User.HasScope(Scopes.Roles))
        {
            claims[Claims.Role] = await userManager.GetValidRolesAsync(user);
        }

        return Ok(claims);
    }

    private static IEnumerable<string> GetDestinations(Claim claim)
    {
        switch (claim.Type)
        {
            case Claims.Name or Claims.PreferredUsername:
                yield return Destinations.AccessToken;
                if (claim.Subject != null && claim.Subject.HasScope(Scopes.Profile))
                    yield return Destinations.IdentityToken;
                yield break;

            case Claims.Email:
                yield return Destinations.AccessToken;
                if (claim.Subject != null && claim.Subject.HasScope(Scopes.Email))
                    yield return Destinations.IdentityToken;
                yield break;

            case Claims.Role:
                yield return Destinations.AccessToken;
                if (claim.Subject != null && claim.Subject.HasScope(Scopes.Roles))
                    yield return Destinations.IdentityToken;
                yield break;

            case Claims.Audience:
                yield return Destinations.AccessToken;
                yield return Destinations.IdentityToken;
                yield break;

            // Never include the security stamp in the access and identity tokens, as it's a secret value.
            case "AspNet.Identity.SecurityStamp": yield break;

            default:
                yield return Destinations.AccessToken;
                yield break;
        }
    }

    private async Task<ClaimsIdentity> CreateClaimsIdentity(OpenIddictRequest request, User user, object application, List<object> authorizations)
    {
        var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType,
            Claims.Name, Claims.Role);

        // 获取应用程序的 client_id 作为 audience
        var audience = await applicationManager.GetClientIdAsync(application);

        identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user))
            .SetClaim(Claims.Name, await userManager.GetUserNameAsync(user))
            .SetClaim(Claims.Email, await userManager.GetEmailAsync(user))
            .SetClaim(Claims.Nickname, user.NickName)
            .SetClaims(Claims.Role, [.. await userManager.GetValidRolesAsync(user)])
            .SetClaim(Claims.Audience, audience)
            // 设置 user_version 声明，使用用户的 SecurityStamp 的 SHA256 哈希值，用于跟踪用户信息及权限的更改
            .SetClaim("user_version", HashHelper.GetSha256(await userManager.GetSecurityStampAsync(user)));

        identity.SetScopes(request.GetScopes());

        identity.SetResources(await scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());

        var authorization = authorizations.LastOrDefault();
        authorization ??= await authorizationManager.CreateAsync(
            identity: identity,
            subject: await userManager.GetUserIdAsync(user),
            client: (await applicationManager.GetIdAsync(application))!,
            type: AuthorizationTypes.Permanent,
            scopes: identity.GetScopes());

        identity.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization));
        identity.SetDestinations(GetDestinations);
        return identity;
    }

    private async Task<ClaimsIdentity> CreateClaimsIdentity(ClaimsPrincipal principal, User user)
    {
        var identity = new ClaimsIdentity(principal.Claims,
            TokenValidationParameters.DefaultAuthenticationType,
            Claims.Name, Claims.Role);
        identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user))
            .SetClaim(Claims.Name, await userManager.GetUserNameAsync(user))
            .SetClaim(Claims.Email, await userManager.GetEmailAsync(user))
            .SetClaim(Claims.Nickname, user.NickName)
            .SetClaims(Claims.Role, [.. await userManager.GetValidRolesAsync(user)])
            .SetClaim("user_version", HashHelper.GetSha256(await userManager.GetSecurityStampAsync(user)));

        identity.SetDestinations(GetDestinations);
        return identity;
    }

    private async Task<User> GetUserAsync(ClaimsPrincipal principal)
    {
        // 检索已登录用户的用户信息
        return await userManager.GetUserAsync(principal)
            ?? throw new InvalidOperationException("The user details cannot be retrieved.");
    }

    private async Task<object> GetApplicationAsync(string clientId)
    {
        // 从数据库中检索应用程序详细信息。
        return await applicationManager.FindByClientIdAsync(clientId)
            ?? throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
    }

    private async Task<List<object>> GetAuthorizations(User user, object application, OpenIddictRequest request)
    {
        // 检索与用户和调用客户端应用程序关联的永久授权。
        return await authorizationManager.FindAsync(
            subject: await userManager.GetUserIdAsync(user),
            client: (await applicationManager.GetIdAsync(application))!,
            status: Statuses.Valid,
            type: AuthorizationTypes.Permanent,
            scopes: request.GetScopes()).ToListAsync();
    }

    private ActionResult Forbid(string error, string description)
    {
        var properties = new AuthenticationProperties(new Dictionary<string, string?>
        {
            [OpenIddictServerAspNetCoreConstants.Properties.Error] = error,
            [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = description
        });
        return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    }

    private ActionResult BadRequest(string error, string description)
    {
        var properties = new AuthenticationProperties(new Dictionary<string, string?>
        {
            [OpenIddictServerAspNetCoreConstants.Properties.Error] = error,
            [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = description
        });
        return BadRequest(properties);
    }
}
